Compare & Ordering(Sort)
#Kotlin #Comparator #Comparable #SortedWith
1. Compare
"1이랑 100 중에 어떤게 더 큰가?"
"apple이랑 zebra 중에 어떤게 사전에서 더 뒤에 오는가?"
이런걸 비교라고 한다.
1-1. Comparable
Comparable Interface 소스코드
Comparable api
위의 두 예제는 비교의 대상이 되는 값의 타입이 각각 Int, String
이다.
이렇게 코틀린의 기본 타입들은 각 값들을 비교할 수 있다. oneVal.compareTo(twoVal)
과 같이 compareTo()
메서드를 사용해서.
Int, String의 구현에 가보면
public class Int private constructor() : Number(), Comparable<Int> {}
public class String : Comparable<String>, CharSequence {}
이렇게 Comparable<T>
를 상속받고 있는것을 확인할 수 있다.
우리가 만든(레퍼런스 타입) 객체들도 비교가 가능하게 만들어 줄 수 있다. Comparable을 사용해서 객체 자체가 비교가 가능한 객체로 인식되도록 만드는 것이다.
public class User(val id: Int, val name: String, val level: Int) : Comparable<User> {
override fun compareTo(other: User): Int {
return when {
this.id > other.id -> 1
this.id < other.id -> -1
else -> 0
}
}
}
fun main() {
val user1 = User(1, "one", 100)
val user2 = User(2, "two", 200)
println("user1 > user2? => user1.compareTo(user2)")
}
=> 객체 자체를 비교 가능한 객체로 만들어 주기 위해 사용하는 인터페이스
객체마다 compare의 기준은 하나씩이다.
그런데 여기서 어떤 다른 기준을 가지고 정렬하고 싶다. name이나 level를 이용해서. 라고 했을 때에는 어떻게 해야할까?
1-2. Comparator
그럴 때 Comparator을 사용하면 된다.
object compareNameTo : Comparator<User> {
override fun compare(user1: User, user2: User): Int {
return when {
user1.id > user2.id -> 1
user1.id < user2.id -> -1
else -> 0
}
}
}
object compareLevelTo : Comparator<User> {
override fun compare(user1: User, user2: User): Int {
return when {
user1.level > user2.level -> 1
user1.level < user2.level -> -1
else -> 0
}
}
}
이것들을 가지고 정렬 메서드에 적용시켜주면 된다.
2. Ordering
Custom Ordering 공식문서
일반적으로 숫자, 알파벳등의 기준으로 정렬을 하고자 하면 sorted()
를 사용하면 된다.
User도 sorted()
로 정렬할 수 있을까?
=> 당연하다. sorted는 comparable인터페이스의 확장함수니까!
이것이 우리가 만든 객체를 Comparable로 만드는 핵심 이유이기도 하다.
그럼 name이나 level 프로퍼티로 정렬하고 싶을때는?
=> sortedBy() or sortedWith()
를 Comparable or Comparator
를 상속받은 객체를 이용해 사용하면 된다.
fun main() {
val user1 = User(1, "one", 100)
val user2 = User(2, "two", 200)
val user3 = User(3, "three", 300)
val userArr = arrayOf(user3, user1, user2)
val sortedByName = userArr.sortedWith(compareNameTo)
println("sorted by user name: ${sortedByName.map{user -> user.id}}")
}
위의 예제에서 왜 sortedBy가 아니라 sortedWith를 썼을까?
2-1. sortedBy()
inline fun <T, R : Comparable<R>> Array<out T>.sortedBy(
crossinline selector: (T) -> R?
): List<T>
sortedBy()의 파라미터는 함수타입이다. 즉 comparator 타입이 아니라는 말이다.
간단하게 재활용을 생각하지 않고 기준만 잡고, 정렬만 해내고 싶을 때 사용하는게 sortedBy()이다.
fun main() {
val sortedByName = userArr.sortedBy{
it.name
}
println("sorted by user name: ${sortedByName.map{user -> user.id}}")
}
2-2. sortedWith()
fun <T> Array<out T>.sortedWith(
comparator: Comparator<in T>
): List<T>
반면 sortedWith()의 파라미터는 Comparator 타입이다.
그럼 왜 굳이굳이 Comparator을 만들어서 사용하느냐?
가장 기본적인 이유로는 재활용성을 꼽을 수 있다.
그리고 대망의 이유는! '여러 기준'을 적용할 수 있다.
Comparator은 확장함수 then()을 가지기 때문에!
예를 들어, level 순으로 정렬한 다음, 같은 level을 가진 유저들을 다시 이름순으로 정렬하고 싶다. 와 같을 때 사용한다.
fun main() {
val chainComparator = compareLevelTo.then(compareNameTo)
val sortedByLevelAndName = userArr.sortedWith(chainComparator)
println("sorted by user level and then name: ${sortedByLevelAndName.map{user -> user.id}}")
}
여기서 하나더.
굳이굳이 재사용을 할 필요가 없는데, 기준을 여러개로 만들고 싶다?
compareBy()
함수를 이용하자. 이것은 Comparable을 리턴해주기만 하면 된다. 여러개를 리턴해도 된다.
compareBy api
fun main() {
val sortedByLevelAndName = userArr.sortedWith(compareBy(
{it.level},
{it.name}
))
println("sorted by user level and then name: ${sortedByLevelAndName.map{user -> user.id}}")
}
이 compareBy는 함수들, 함수 하나, comparator+함수를 파라미터로 받을 수 있다.
그리고 compareBy()는 결국 comparator를 반환한다.